Skip to content

feat: parse parameter decorators for transforms#3001

Open
jtenner wants to merge 15 commits intoAssemblyScript:mainfrom
jtenner:parse-parameter-decorators
Open

feat: parse parameter decorators for transforms#3001
jtenner wants to merge 15 commits intoAssemblyScript:mainfrom
jtenner:parse-parameter-decorators

Conversation

@jtenner
Copy link
Copy Markdown
Contributor

@jtenner jtenner commented Mar 27, 2026

Summary

This PR makes parameter decorators parseable and preservable in the AST so transforms can inspect or remove them before compilation-time validation runs.

This is intentionally not a new end-user language feature. Any parameter decorators that survive transform time still produce TS1206: Decorators are not valid here.

What Changed

  • added AST storage for parameter decorators on ParameterNode
  • added AST storage for explicit-this parameter decorators on FunctionTypeNode
  • taught the shared parameter parser to preserve decorators across function declarations, methods, constructors, function types, function expressions, and parenthesized arrow functions
  • updated the AST serializer to round-trip preserved parameter decorators
  • added a Program validation pass that rejects surviving parameter decorators once per decorated parameter using the full decorator-list range
  • ran that validation after afterInitialize, so transforms can remove the decorators first
  • added parser, compiler, and transform coverage for accepted syntax, delayed rejection, and transform-time removal
  • documented the transform hook timing in the transform-facing API comments and example transforms

Why

The parser and transform pipeline can use preserved parameter decorators as an AST-level extension point without committing AssemblyScript to supporting them as regular language syntax.

That gives transform authors a useful hook while keeping the language semantics unchanged:

  • transforms can read or erase parameter decorators
  • the compiler still rejects them if they remain after transform time
  • public-facing docs do not need to advertise parameter decorators as supported syntax

Impact

  • transform authors can now observe and rewrite parameter decorators before validation
  • end users still get TS1206 if parameter decorators survive to compilation
  • constructor parameter properties continue to receive no special decorator semantics

Validation

  • npm run build
  • npm run test:parser -- parameter-decorators
  • npm run test:compiler -- parameter-decorators --noDiff
  • npm run test:transform

@jtenner jtenner changed the title [codex] Parse parameter decorators for transforms feat: parse parameter decorators for transforms Mar 27, 2026
@jtenner jtenner marked this pull request as ready for review March 27, 2026 04:35
@jtenner
Copy link
Copy Markdown
Contributor Author

jtenner commented Mar 27, 2026

Whoops! I forgot to check the validation scripts locally. Everything looks good now.

I hope this contribution finds it's way to main because I want to use it to make some very cheeky transforms for my webgpu implementation.

@jtenner jtenner closed this Apr 5, 2026
@jtenner jtenner force-pushed the parse-parameter-decorators branch from 4ec3371 to 4786023 Compare April 5, 2026 20:33
@jtenner jtenner reopened this Apr 5, 2026
@jtenner
Copy link
Copy Markdown
Contributor Author

jtenner commented Apr 5, 2026

Is this all set? I rebased it on main and had to add the eslint "ignore" file because of the decorator proof. I left a comment to explain why. Please let me know if anything isn't up to standard!

@PaperPrototype
Copy link
Copy Markdown

👀 are you planning on using this for WGSL/GLSL translation?

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables parsing and AST preservation of parameter decorators (including decorators on explicit this) so transforms can inspect/remove them before compilation-time validation, while keeping parameter decorators invalid if they survive past transform time.

Changes:

  • Extend the AST (ParameterNode, FunctionTypeNode) and serializer to round-trip preserved parameter decorators.
  • Update the parser to accept/preserve parameter decorators across function declarations/expressions, methods, constructors, and function types (including explicit this).
  • Add a post-transform validation pass that emits TS1206 for any remaining parameter decorators, plus parser/compiler/transform tests and docs timing updates.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ast.ts Adds storage for parameter decorators on ParameterNode and explicit-this decorators on FunctionTypeNode.
src/parser.ts Parses and preserves parameter decorators in parameter lists and function types; threads explicit-this decorators through function expression parsing.
src/extra/ast.ts Serializes preserved parameter decorators so they can be round-tripped.
src/program.ts Implements a one-shot AST traversal to report surviving parameter decorators after transforms.
src/compiler.ts Runs the new post-transform parameter-decorator validation after initialization.
src/index-wasm.ts Clarifies initializeProgram timing for transforms relative to validation.
cli/index.js Documents afterInitialize as the last AST rewrite point before compilation-time validation.
cli/index.d.ts Updates transform API docs to clarify afterInitialize timing and intent.
tests/parser/parameter-decorators.ts Adds parser coverage for parameter decorator syntax across constructs.
tests/parser/parameter-decorators.ts.fixture.ts Adds parser fixture inputs covering multiple decorators, this, and rest parameters.
tests/compiler/parameter-decorators.ts Adds compiler test ensuring TS1206 is emitted if decorators survive.
tests/compiler/parameter-decorators.json Expected stderr for surviving parameter decorator validation.
tests/compiler/parameter-decorators-errors.ts Adds invalid-syntax compiler coverage for decorator placement/forms.
tests/compiler/parameter-decorators-errors.json Expected stderr for invalid parameter decorator syntax cases.
tests/transform/parameter-decorators.ts Transform test input program using parameter decorators (including on this).
tests/transform/remove-parameter-decorators.js ESM transform that strips preserved parameter decorators during afterInitialize.
tests/transform/cjs/remove-parameter-decorators.js CommonJS transform that strips preserved parameter decorators during afterInitialize.
package.json Extends transform test scripts to cover decorator stripping behavior in ESM+CJS transforms.
eslint.config.js Ignores the transform fixture that is intentionally invalid TypeScript syntax.
.eslintrc.cjs Adds a legacy ESLint config file (in addition to existing flat config).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.eslintrc.cjs Outdated
Comment on lines +1 to +12
/* global module */

module.exports = {
root: true,
ignorePatterns: [
// These fixtures intentionally exercise AssemblyScript-only syntax that
// TypeScript's parser rejects before lint rules can run.
"tests/compiler/parameter-decorators.ts",
"tests/transform/parameter-decorators.ts"
],
parser: "@typescript-eslint/parser",
plugins: [
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a full legacy .eslintrc.cjs alongside the existing flat eslint.config.js configuration. With ESLint v10 (as pinned in package.json), eslint.config.js is the active config and .eslintrc.* is typically ignored unless users opt out of flat config. Consider removing this file or documenting why it’s needed to avoid confusing contributors with two independent ESLint configurations.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, why we need this new eslintrc file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this was a mistake.

Comment on lines -184 to 186
range: Range
range: Range,
decorators: DecoratorNode[] | null = null
): ParameterNode {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think range should be always at end.

  decorators: DecoratorNode[] | null,
  range: Range,
): ParameterNode {

And yes, this will lead to more refactorings

Copy link
Copy Markdown
Contributor Author

@jtenner jtenner Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All set. 👍

@MaxGraey
Copy link
Copy Markdown
Member

MaxGraey commented Apr 8, 2026

Copilot creates more chaos and noise than it actually provides in a useful review. Not worth it.

@HerrCai0907
Copy link
Copy Markdown
Member

Copilot creates more chaos and noise than it actually provides in a useful review. Not worth it.

It is quiet unstable, sometime is pretty good but sometime totally wrong.

@MaxGraey
Copy link
Copy Markdown
Member

MaxGraey commented Apr 10, 2026

Copilot = outdated GPT-4 + telemetry & logging content to Microsoft + terrible post train + run on Azure

Sometimes Copilot even took screenshots (but only for the desktop)

In short, main goal is not helping you

jtenner added 8 commits April 12, 2026 09:22
Extend parameter AST nodes to retain decorators, including explicit-this decorators on function signatures, without forcing a broad constructor API churn.

Teach both parameter parsers to accept stacked decorators on regular, rest, explicit-this, constructor-property, function-type, and parenthesized-arrow parameters.

Update the AST builder to serialize parameter decorators inline so parser fixtures can round-trip the new syntax faithfully.

Add a focused parser fixture covering the preserved syntax surface before deferred validation is introduced.
Add a Program-owned validation pass that walks the final AST and reports TS1206 once per decorated parameter, using the full decorator-list span for the diagnostic range.

Invoke that validation from compile after transforms have had their afterInitialize window, so transformers can still remove parameter decorators before any diagnostics are emitted.

Add a dedicated compiler rejection fixture covering regular, rest, explicit-this, constructor-property, function-type, function-expression, and arrow-parameter cases.
Add a dedicated transform input containing the same invalid parameter-decorator forms exercised by the compiler rejection fixture.

Introduce ESM and CommonJS afterInitialize transforms that walk the AST and strip parameter decorators, including explicit-this decorators on function signatures.

Extend the transform test scripts to compile that input with the stripping transforms, proving no TS1206 diagnostics are emitted once transforms remove the decorators in time.
@jtenner jtenner force-pushed the parse-parameter-decorators branch from 03925af to c267d6e Compare April 12, 2026 13:22
Comment on lines +1534 to +1913
/** Rejects parameter decorators that survive transform time. These remain transform-only syntax. */
validateParameterDecorators(): void {
if (this.parameterDecoratorsValidated) return;
this.parameterDecoratorsValidated = true;
let sources = this.sources;
for (let i = 0, k = sources.length; i < k; ++i) {
this.validateParameterDecoratorsInSource(sources[i]);
}
}

private validateParameterDecoratorsInSource(source: Source): void {
let statements = source.statements;
for (let i = 0, k = statements.length; i < k; ++i) {
this.validateParameterDecoratorsInStatement(statements[i]);
}
}

private validateParameterDecoratorsInStatements(statements: Statement[] | null): void {
if (statements) {
for (let i = 0, k = statements.length; i < k; ++i) {
this.validateParameterDecoratorsInStatement(statements[i]);
}
}
}

private validateParameterDecoratorsInStatement(statement: Statement | null): void {
if (!statement) return;
switch (statement.kind) {
case NodeKind.Block: {
this.validateParameterDecoratorsInStatements((<BlockStatement>statement).statements);
break;
}
case NodeKind.ClassDeclaration:
case NodeKind.InterfaceDeclaration: {
this.validateParameterDecoratorsInClassDeclaration(<ClassDeclaration>statement);
break;
}
case NodeKind.Do: {
let doStatement = <DoStatement>statement;
this.validateParameterDecoratorsInStatement(doStatement.body);
this.validateParameterDecoratorsInExpression(doStatement.condition);
break;
}
case NodeKind.EnumDeclaration: {
this.validateParameterDecoratorsInEnumDeclaration(<EnumDeclaration>statement);
break;
}
case NodeKind.ExportDefault: {
this.validateParameterDecoratorsInDeclaration((<ExportDefaultStatement>statement).declaration);
break;
}
case NodeKind.Expression: {
this.validateParameterDecoratorsInExpression((<ExpressionStatement>statement).expression);
break;
}
case NodeKind.For: {
let forStatement = <ForStatement>statement;
this.validateParameterDecoratorsInStatement(forStatement.initializer);
this.validateParameterDecoratorsInExpression(forStatement.condition);
this.validateParameterDecoratorsInExpression(forStatement.incrementor);
this.validateParameterDecoratorsInStatement(forStatement.body);
break;
}
case NodeKind.ForOf: {
let forOfStatement = <ForOfStatement>statement;
this.validateParameterDecoratorsInStatement(forOfStatement.variable);
this.validateParameterDecoratorsInExpression(forOfStatement.iterable);
this.validateParameterDecoratorsInStatement(forOfStatement.body);
break;
}
case NodeKind.FunctionDeclaration:
case NodeKind.MethodDeclaration: {
this.validateParameterDecoratorsInFunctionDeclaration(<FunctionDeclaration>statement);
break;
}
case NodeKind.If: {
let ifStatement = <IfStatement>statement;
this.validateParameterDecoratorsInExpression(ifStatement.condition);
this.validateParameterDecoratorsInStatement(ifStatement.ifTrue);
this.validateParameterDecoratorsInStatement(ifStatement.ifFalse);
break;
}
case NodeKind.NamespaceDeclaration: {
this.validateParameterDecoratorsInStatements((<NamespaceDeclaration>statement).members);
break;
}
case NodeKind.Return: {
this.validateParameterDecoratorsInExpression((<ReturnStatement>statement).value);
break;
}
case NodeKind.Switch: {
let switchStatement = <SwitchStatement>statement;
this.validateParameterDecoratorsInExpression(switchStatement.condition);
let cases = switchStatement.cases;
for (let i = 0, k = cases.length; i < k; ++i) {
this.validateParameterDecoratorsInSwitchCase(cases[i]);
}
break;
}
case NodeKind.Throw: {
this.validateParameterDecoratorsInExpression((<ThrowStatement>statement).value);
break;
}
case NodeKind.Try: {
let tryStatement = <TryStatement>statement;
this.validateParameterDecoratorsInStatements(tryStatement.bodyStatements);
this.validateParameterDecoratorsInStatements(tryStatement.catchStatements);
this.validateParameterDecoratorsInStatements(tryStatement.finallyStatements);
break;
}
case NodeKind.TypeDeclaration: {
this.validateParameterDecoratorsInTypeDeclaration(<TypeDeclaration>statement);
break;
}
case NodeKind.Variable: {
let declarations = (<VariableStatement>statement).declarations;
for (let i = 0, k = declarations.length; i < k; ++i) {
this.validateParameterDecoratorsInVariableLikeDeclaration(declarations[i]);
}
break;
}
case NodeKind.Void: {
this.validateParameterDecoratorsInExpression((<VoidStatement>statement).expression);
break;
}
case NodeKind.While: {
let whileStatement = <WhileStatement>statement;
this.validateParameterDecoratorsInExpression(whileStatement.condition);
this.validateParameterDecoratorsInStatement(whileStatement.body);
break;
}
}
}

private validateParameterDecoratorsInDeclaration(declaration: DeclarationStatement): void {
switch (declaration.kind) {
case NodeKind.ClassDeclaration:
case NodeKind.InterfaceDeclaration: {
this.validateParameterDecoratorsInClassDeclaration(<ClassDeclaration>declaration);
break;
}
case NodeKind.EnumDeclaration: {
this.validateParameterDecoratorsInEnumDeclaration(<EnumDeclaration>declaration);
break;
}
case NodeKind.FieldDeclaration:
case NodeKind.VariableDeclaration: {
this.validateParameterDecoratorsInVariableLikeDeclaration(<VariableLikeDeclarationStatement>declaration);
break;
}
case NodeKind.FunctionDeclaration:
case NodeKind.MethodDeclaration: {
this.validateParameterDecoratorsInFunctionDeclaration(<FunctionDeclaration>declaration);
break;
}
case NodeKind.NamespaceDeclaration: {
this.validateParameterDecoratorsInStatements((<NamespaceDeclaration>declaration).members);
break;
}
case NodeKind.TypeDeclaration: {
this.validateParameterDecoratorsInTypeDeclaration(<TypeDeclaration>declaration);
break;
}
}
}

private validateParameterDecoratorsInClassDeclaration(node: ClassDeclaration): void {
this.validateParameterDecoratorsInTypeParameters(node.typeParameters);
this.validateParameterDecoratorsInType(node.extendsType);
let implementsTypes = node.implementsTypes;
if (implementsTypes) {
for (let i = 0, k = implementsTypes.length; i < k; ++i) {
this.validateParameterDecoratorsInType(implementsTypes[i]);
}
}
this.validateParameterDecoratorsInIndexSignature(node.indexSignature);
let members = node.members;
for (let i = 0, k = members.length; i < k; ++i) {
this.validateParameterDecoratorsInDeclaration(members[i]);
}
}

private validateParameterDecoratorsInEnumDeclaration(node: EnumDeclaration): void {
let values = node.values;
for (let i = 0, k = values.length; i < k; ++i) {
this.validateParameterDecoratorsInVariableLikeDeclaration(values[i]);
}
}

private validateParameterDecoratorsInFunctionDeclaration(node: FunctionDeclaration): void {
this.validateParameterDecoratorsInTypeParameters(node.typeParameters);
this.validateParameterDecoratorsInFunctionType(node.signature);
this.validateParameterDecoratorsInStatement(node.body);
}

private validateParameterDecoratorsInTypeDeclaration(node: TypeDeclaration): void {
this.validateParameterDecoratorsInTypeParameters(node.typeParameters);
this.validateParameterDecoratorsInType(node.type);
}

private validateParameterDecoratorsInVariableLikeDeclaration(node: VariableLikeDeclarationStatement): void {
this.validateParameterDecoratorsInType(node.type);
this.validateParameterDecoratorsInExpression(node.initializer);
}

private validateParameterDecoratorsInSwitchCase(node: SwitchCase): void {
this.validateParameterDecoratorsInExpression(node.label);
this.validateParameterDecoratorsInStatements(node.statements);
}

private validateParameterDecoratorsInIndexSignature(node: IndexSignatureNode | null): void {
if (!node) return;
this.validateParameterDecoratorsInType(node.keyType);
this.validateParameterDecoratorsInType(node.valueType);
}

private validateParameterDecoratorsInExpression(node: Expression | null): void {
if (!node) return;
switch (node.kind) {
case NodeKind.Assertion: {
let assertion = <AssertionExpression>node;
this.validateParameterDecoratorsInExpression(assertion.expression);
this.validateParameterDecoratorsInType(assertion.toType);
break;
}
case NodeKind.Binary: {
let binary = <BinaryExpression>node;
this.validateParameterDecoratorsInExpression(binary.left);
this.validateParameterDecoratorsInExpression(binary.right);
break;
}
case NodeKind.Call: {
let call = <CallExpression>node;
this.validateParameterDecoratorsInExpression(call.expression);
this.validateParameterDecoratorsInTypes(call.typeArguments);
this.validateParameterDecoratorsInExpressions(call.args);
break;
}
case NodeKind.Class: {
this.validateParameterDecoratorsInClassDeclaration((<ClassExpression>node).declaration);
break;
}
case NodeKind.Comma: {
this.validateParameterDecoratorsInExpressions((<CommaExpression>node).expressions);
break;
}
case NodeKind.ElementAccess: {
let elementAccess = <ElementAccessExpression>node;
this.validateParameterDecoratorsInExpression(elementAccess.expression);
this.validateParameterDecoratorsInExpression(elementAccess.elementExpression);
break;
}
case NodeKind.Function: {
this.validateParameterDecoratorsInFunctionDeclaration((<FunctionExpression>node).declaration);
break;
}
case NodeKind.InstanceOf: {
let instanceOf = <InstanceOfExpression>node;
this.validateParameterDecoratorsInExpression(instanceOf.expression);
this.validateParameterDecoratorsInType(instanceOf.isType);
break;
}
case NodeKind.Literal: {
this.validateParameterDecoratorsInLiteral(<LiteralExpression>node);
break;
}
case NodeKind.New: {
let newExpression = <NewExpression>node;
this.validateParameterDecoratorsInTypes(newExpression.typeArguments);
this.validateParameterDecoratorsInExpressions(newExpression.args);
break;
}
case NodeKind.Parenthesized: {
this.validateParameterDecoratorsInExpression((<ParenthesizedExpression>node).expression);
break;
}
case NodeKind.PropertyAccess: {
this.validateParameterDecoratorsInExpression((<PropertyAccessExpression>node).expression);
break;
}
case NodeKind.Ternary: {
let ternary = <TernaryExpression>node;
this.validateParameterDecoratorsInExpression(ternary.condition);
this.validateParameterDecoratorsInExpression(ternary.ifThen);
this.validateParameterDecoratorsInExpression(ternary.ifElse);
break;
}
case NodeKind.UnaryPostfix: {
this.validateParameterDecoratorsInExpression((<UnaryPostfixExpression>node).operand);
break;
}
case NodeKind.UnaryPrefix: {
this.validateParameterDecoratorsInExpression((<UnaryPrefixExpression>node).operand);
break;
}
}
}

private validateParameterDecoratorsInExpressions(nodes: Expression[] | null): void {
if (nodes) {
for (let i = 0, k = nodes.length; i < k; ++i) {
this.validateParameterDecoratorsInExpression(nodes[i]);
}
}
}

private validateParameterDecoratorsInLiteral(node: LiteralExpression): void {
switch (node.literalKind) {
case LiteralKind.Array: {
this.validateParameterDecoratorsInExpressions((<ArrayLiteralExpression>node).elementExpressions);
break;
}
case LiteralKind.Object: {
this.validateParameterDecoratorsInExpressions((<ObjectLiteralExpression>node).values);
break;
}
case LiteralKind.Template: {
this.validateParameterDecoratorsInExpressions((<TemplateLiteralExpression>node).expressions);
break;
}
}
}

private validateParameterDecoratorsInType(node: TypeNode | null): void {
if (!node) return;
switch (node.kind) {
case NodeKind.FunctionType: {
this.validateParameterDecoratorsInFunctionType(<FunctionTypeNode>node);
break;
}
case NodeKind.NamedType: {
this.validateParameterDecoratorsInTypes((<NamedTypeNode>node).typeArguments);
break;
}
}
}

private validateParameterDecoratorsInTypes(nodes: TypeNode[] | null): void {
if (nodes) {
for (let i = 0, k = nodes.length; i < k; ++i) {
this.validateParameterDecoratorsInType(nodes[i]);
}
}
}

private validateParameterDecoratorsInTypeParameters(nodes: TypeParameterNode[] | null): void {
if (nodes) {
for (let i = 0, k = nodes.length; i < k; ++i) {
let node = nodes[i];
this.validateParameterDecoratorsInType(node.extendsType);
this.validateParameterDecoratorsInType(node.defaultType);
}
}
}

private validateParameterDecoratorsInFunctionType(node: FunctionTypeNode): void {
this.reportParameterDecorators(node.explicitThisDecorators);
this.validateParameterDecoratorsInType(node.explicitThisType);
let parameters = node.parameters;
for (let i = 0, k = parameters.length; i < k; ++i) {
this.validateParameterDecoratorsInParameter(parameters[i]);
}
this.validateParameterDecoratorsInType(node.returnType);
}

private validateParameterDecoratorsInParameter(node: ParameterNode): void {
this.reportParameterDecorators(node.decorators);
this.validateParameterDecoratorsInType(node.type);
this.validateParameterDecoratorsInExpression(node.initializer);
}

private reportParameterDecorators(decorators: DecoratorNode[] | null): void {
if (decorators && decorators.length > 0) {
this.error(
DiagnosticCode.Decorators_are_not_valid_here,
Range.join(decorators[0].range, decorators[decorators.length - 1].range)
);
}
}

Copy link
Copy Markdown
Member

@dcodeIO dcodeIO Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite a lot of code (almost a complete AST pass) for a relatively simple feature. There's probably a more compact and general approach that can take its place, like during an earlier pass maintaining a set of references to decorators (or decorator-containing parents if necessary), and after initalize walk just this set to see what remains. Otherwise, if we did it like this, we'd ultimately end up with a bunch of adhoc passes eventually, that all need to be updated whenever the AST changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw I also thought it would be better to extend the existing methods for parsing decorators. We could add a boolean operator to indicate that it is indeed a parameter. Although we need to assess how much overlap there is. @jtenner have you considered that opportunity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im unsure what @MaxGraey means, but I think this should be consolidated instead of added as a pass now that I look at it. Any ideas?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I took this route in 6f75b5e.

Instead of keeping a bespoke recursive pass in Program, I split the problem into three parts:

  1. parser.ts records which source-level statements actually preserved parameter decorators while parsing,
  2. ast.ts owns subtree traversal through a small shared NodeWalker, and
  3. program.ts now just runs a tiny parameter-specific validator after afterInitialize.

The main reason I chose this approach was to get AST topology knowledge out of Program and into one shared traversal utility, so we do not keep accumulating feature-specific recursive passes there as the AST evolves. That seemed like the best way to address the maintainability concern you raised while still keeping the transform timing/behavior the same.

If you would prefer this to validate by walking all sources after afterInitialize, or if there is a better place/shape for the walker, I am happy to adjust. Any feedback or suggestions would be appreciated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is among the more usable fictions of the present moment that cognition itself can be cleanly externalized into an adjacent intelligence, though the very convenience of that displacement is precisely what renders it corrosive: one entrusts thought to the apparatus only to discover, in the flattening light of that delegation, the full contour of one's accumulated judgment already exposed, already spent, already standing outside itself.

jtenner added 2 commits April 13, 2026 17:59
Replace the bespoke parameter-decorator validation pass in Program with a small validator built on top of a shared AST walker.

The previous implementation worked, but it reimplemented a near-complete AST traversal inside program.ts for one feature. That made Program responsible for AST topology, duplicated traversal logic, and created exactly the kind of ad hoc pass review feedback called out: every AST shape change would have to remember to update this validator as well.

This change separates three concerns more cleanly:

- parser.ts records which source-level statements actually preserved parameter decorators while parsing
- ast.ts owns subtree traversal through a generic NodeWalker utility
- program.ts only performs the parameter-specific check after afterInitialize

In other words, the parser decides what roots are relevant, the walker decides how to descend the tree, and the validator decides what to report.

This is better because it keeps AST traversal knowledge in one place, removes a large one-off recursive pass from Program, and makes future AST validation/analysis passes much smaller if they need to inspect subtrees later.

The validator still runs after afterInitialize so transforms can inspect or remove preserved parameter decorators first, and the tests continue to cover parser round-tripping, compiler rejection, and transform-time removal.

Also add a namespace regression case to ensure preserved parameter decorators nested under a tracked source-level statement are still validated and can still be stripped by transforms.
Drop AST imports from src/program.ts that became unused after moving subtree traversal into NodeWalker. This fixes the lint warnings reported in CI and keeps the parameter decorator refactor clean.
@jtenner
Copy link
Copy Markdown
Contributor Author

jtenner commented Apr 13, 2026

Follow-up: I fixed the lint warnings from the refactor by removing the now-unused AST imports in src/program.ts (53dc57c). I also reran full validation locally: npm run check, npm run build, and npm test all passed.

jtenner added 2 commits April 13, 2026 18:21
Follow the existing AST convention that Range is the last constructor field and the last factory/helper parameter. This updates ParameterNode and Node.createParameter accordingly, and adjusts all call sites.
Account for explicit-this parameter decorators in the same way as parameter decorators: wire explicitThisDecorators through FunctionTypeNode and Node.createFunctionType, keep it adjacent to explicitThisType, and leave range as the last constructor/helper parameter. Update all call sites accordingly.
src/extra/ast.ts Outdated
Comment on lines +1576 to +1594
private visitParameterDecorator(node: DecoratorNode): void {
let sb = this.sb;
sb.push("@");
this.visitNode(node.name);
let args = node.args;
if (args) {
sb.push("(");
let numArgs = args.length;
if (numArgs) {
this.visitNode(args[0]);
for (let i = 1; i < numArgs; ++i) {
sb.push(", ");
this.visitNode(args[i]);
}
}
sb.push(")");
}
sb.push(" ");
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's literally very similar to serializeDecorator. I suggest combining this such similar methods for other parts into a single one serializeDecorator (or visitDecorator), and accounting for the difference in logic with an optional second parameter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All set. 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, thank you for explaining. It wasn't clear to me what you meant, and this was good feedback!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there’s something similar somewhere else. Try to investigate more. Also, you could delegate this to LLM with good reasoning

Remove the duplicate parameter-decorator serializer in ASTBuilder and fold the inline formatting case into serializeDecorator with an optional flag. This keeps decorator serialization in one place while preserving the spacing differences between declaration decorators and parameter decorators.
Comment on lines +962 to +974
private parseParameterDecorators(
tn: Tokenizer
): DecoratorNode[] | null {
// Preserve parameter decorators in the AST so transforms can inspect or remove them later.
let decorators: DecoratorNode[] | null = null;
while (tn.skip(Token.At)) {
let decorator = this.parseDecorator(tn);
if (!decorator) break;
if (!decorators) decorators = [decorator];
else decorators.push(decorator);
}
return decorators;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, I think it's a good candidate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good candidate for what?

Copy link
Copy Markdown
Member

@MaxGraey MaxGraey Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It used only once. It could be inclined I guess. Or this already has some helper. I don't remember

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make a suggestion please? I'm sorry I don't understand.

Copy link
Copy Markdown
Member

@MaxGraey MaxGraey Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseParameterDecorators has only one usage and quite small. Why we need parseParameterDecorators function? Just manually inline it

src/parser.ts Outdated
}

/** Consumes decorators that appear after a parameter has already started. */
private reportInvalidParameterDecorators(tn: Tokenizer): void {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private reportInvalidParameterDecorators(tn: Tokenizer): void {
private tryParseParameterDecorators(tn: Tokenizer): void {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All set. 👍

// LogicalOr // a || b
}

class ParameterDecoratorValidator extends NodeWalker {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a NodeWalker? And why we can't do validation after finishing parsing of decorators?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do parsing of the decorators they need to survive to become transformed. If they exist after the transform phase, that is when validation should kick in.

Unless I misunderstand how things work?

Copy link
Copy Markdown
Member

@MaxGraey MaxGraey Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it has usually done via "error parser recovery" mechanics. As far as I can recall, AssemblyScript also follows this approach to some extent. The idea is that you carry on parsing: if you encounter an error, for example missed closing bracket, you're print the error and close bracket by yourself, then continue parsing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxGraey If there is a parsing error, does a transform still work?

Copy link
Copy Markdown
Member

@MaxGraey MaxGraey Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should transform work if we can't properly parse decorators?
As I already said, the parser is designed in such a way that it doesn't stop if errors occur. It tries to fix everything automatically and continue parsing. So, theoretically, you can pass incorrect AST to transformer for instance. But why?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context: The general idea behind the design is that one parse discovers all the parse errors in one go, instead of failing early, and only discovering more as one fixes the source. Is for diagnostics, not implying that when there is a parse error, the source is actually attempted to be compiled.

Rename the parser helper that consumes and reports decorators found after a parameter has already started from reportInvalidParameterDecorators to tryParseParameterDecorators, matching its behavior more closely and aligning with the review suggestion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants